Discovering knowledge in customer shopping behaviors¶

Course: DAMI330484_22_2_01¶

Instructor: M.Sc. Nguyen Van Thanh¶

Group 19
Đỗ Hoàng Thịnh 20133122
Nguyễn Minh Tiến 20133093
Huỳnh Nguyễn Tín 20133094
Bùi Lê Hải Triều 20133101

1. Dataset¶

Nhóm sử dụng tập dữ liệu chứa thông tin giao dịch của khách hàng từ 10 trung tâm mua sắm lớn tại đất nước Istanbul, từ năm 2021 đến thời điểm hiện tại năm 2023 trên Kaggle. Ngoài thông tin giao dịch, tập dữ liệu cũng cung cấp thông tin về độ tuổi, giới tính, phù hợp với nghiệp vụ khai phá.

In [95]:
import matplotlib.pyplot as plt
import pandas as pd
In [96]:
transactions = pd.read_csv('data/transactions.csv')
transactions.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 99457 entries, 0 to 99456
Data columns (total 10 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   invoice_no      99457 non-null  object 
 1   customer_id     99457 non-null  object 
 2   gender          99457 non-null  object 
 3   age             99457 non-null  int64  
 4   category        99457 non-null  object 
 5   quantity        99457 non-null  int64  
 6   price           99457 non-null  float64
 7   payment_method  99457 non-null  object 
 8   invoice_date    99457 non-null  object 
 9   shopping_mall   99457 non-null  object 
dtypes: float64(1), int64(2), object(7)
memory usage: 7.6+ MB

Tập dữ liệu có 99457 giao dịch và 10 cột.

Attribute Description Example Data type
invoice_no Mã giao dịch I138884 Categorical
customer_id Mã khách hàng C241288 Categorical
gender Giới tính Male, Female Categorical
age Độ tuổi 18, 69 Numerical
category Danh mục sản phẩm Clothing Categorical
quantity Số lượng sản phẩm trong giao dịch 1, 5 Numerical
price Đơn giá sản phẩm trong giao dịch 1500.4 Numerical
payment_method Phương thức thanh toán Cash, Credit Card, Debit Card Categorical
invoice_date Ngày diễn ra giao dịch 5/8/2022 Categorical
shopping_mall Địa điểm diễn ra giao dịch Kanyon Categorical
In [97]:
transactions.sample(5)
Out[97]:
invoice_no customer_id gender age category quantity price payment_method invoice_date shopping_mall
92256 I654927 C308067 Male 23 Toys 3 107.52 Cash 15/01/2023 Kanyon
84153 I321680 C453411 Female 61 Cosmetics 2 81.32 Credit Card 12/1/2022 Metrocity
31812 I208020 C126610 Female 68 Clothing 2 600.16 Credit Card 6/6/2021 Kanyon
17405 I101596 C309263 Female 43 Books 5 75.75 Cash 30/08/2021 Kanyon
33833 I167707 C961101 Male 35 Clothing 1 300.08 Cash 8/12/2022 Metropol AVM
In [98]:
transactions.isnull().sum()
Out[98]:
invoice_no        0
customer_id       0
gender            0
age               0
category          0
quantity          0
price             0
payment_method    0
invoice_date      0
shopping_mall     0
dtype: int64
In [99]:
transactions.duplicated().sum()
Out[99]:
0

Tập dữ liệu không chứa giá trị null ở bất kỳ cột nào và không có giao dịch trùng lặp.

2. Data preparation¶

Để phục vụ việc khai phá về sau, nhóm sẽ tạo cột mới chứa thông tin tổng số tiền thanh toán trên mỗi giao dịch.

In [100]:
transactions['total'] = transactions['quantity'] * transactions['price']
transactions.sample(5)
Out[100]:
invoice_no customer_id gender age category quantity price payment_method invoice_date shopping_mall total
41358 I131628 C323483 Male 53 Toys 1 35.84 Cash 13/01/2022 Metrocity 35.84
29627 I134426 C127925 Male 47 Cosmetics 4 162.64 Credit Card 13/04/2022 Kanyon 650.56
30530 I225225 C511061 Male 36 Cosmetics 5 203.30 Cash 4/12/2021 Emaar Square Mall 1016.50
81535 I306915 C184292 Male 65 Souvenir 5 58.65 Cash 19/09/2021 Istinye Park 293.25
73970 I900498 C828029 Male 50 Clothing 3 900.24 Credit Card 19/02/2023 Kanyon 2700.72

Nhóm cũng sẽ thực hiện nhóm tuổi khách hàng thành 6 độ tuổi để giảm độ nhiễu của tập dữ liệu: 18 đến 24, 25 đến 34, 35 đến 44, 45 đến 54, 55 đến 64, và 65 đến 70.

In [101]:
bins = [18, 24, 34, 44, 54, 64, 70]
labels = ['18-24', '25-34', '35-44', '45-54', '55-64', '65-70']
transactions['age_group'] = pd.cut(transactions['age'], bins=bins, labels=labels)
age_group_type = pd.CategoricalDtype(labels, ordered=True)
transactions['age_group'] = transactions['age_group'].astype(age_group_type)
transactions.drop('age', axis=1, inplace=True)
transactions.sample(5)
Out[101]:
invoice_no customer_id gender category quantity price payment_method invoice_date shopping_mall total age_group
8558 I701549 C939167 Female Shoes 1 600.17 Credit Card 24/09/2022 Zorlu Center 600.17 45-54
80651 I322320 C298578 Female Food and Beverage 4 20.92 Debit Card 14/05/2021 Mall of Istanbul 83.68 45-54
83457 I614717 C800700 Male Technology 5 5250.00 Debit Card 4/8/2021 Kanyon 26250.00 45-54
28625 I252146 C992348 Female Cosmetics 3 121.98 Credit Card 20/11/2022 Mall of Istanbul 365.94 55-64
87439 I394044 C108674 Female Souvenir 4 46.92 Debit Card 27/02/2021 Kanyon 187.68 55-64

Nhóm có thể giảm lượng dữ liệu qua việc loại bỏ cột không mang ý nghĩa khai phá như mã giao dịch và mã khách hàng.

In [102]:
transactions.duplicated(subset=['invoice_no']).any()
Out[102]:
False
In [103]:
transactions.duplicated(subset=['customer_id']).any()
Out[103]:
False

Tập dữ liệu không có giao dịch với cùng mã giao dịch hoặc cùng mã khách hàng. Điều này có nghĩa mỗi khách hàng chỉ thực hiện giao dịch một lần. Vì vậy, nhóm có thể loại bỏ hai cột này.

In [104]:
transactions.drop(['invoice_no', 'customer_id'], axis=1, inplace=True)
transactions.sample(5)
Out[104]:
gender category quantity price payment_method invoice_date shopping_mall total age_group
22513 Female Toys 3 107.52 Credit Card 14/08/2022 Metropol AVM 322.56 55-64
1473 Male Toys 5 179.20 Cash 22/09/2022 Metrocity 896.00 35-44
14754 Male Shoes 3 1800.51 Cash 12/6/2021 Metrocity 5401.53 18-24
56969 Male Shoes 4 2400.68 Cash 20/01/2021 Kanyon 9602.72 55-64
31249 Male Food and Beverage 2 10.46 Cash 7/10/2021 Metrocity 20.92 35-44

Kiểm tra số lượng giao dịch trùng lặp sau khi loại bỏ hai cột trên.

In [105]:
transactions.duplicated().sum()
Out[105]:
1111
In [106]:
transactions.drop_duplicates(keep='first')
Out[106]:
gender category quantity price payment_method invoice_date shopping_mall total age_group
0 Female Clothing 5 1500.40 Credit Card 5/8/2022 Kanyon 7502.00 25-34
1 Male Shoes 3 1800.51 Debit Card 12/12/2021 Forum Istanbul 5401.53 18-24
2 Male Clothing 1 300.08 Cash 9/11/2021 Metrocity 300.08 18-24
3 Female Shoes 5 3000.85 Credit Card 16/05/2021 Metropol AVM 15004.25 65-70
4 Female Books 4 60.60 Cash 24/10/2021 Kanyon 242.40 45-54
... ... ... ... ... ... ... ... ... ...
99452 Female Souvenir 5 58.65 Credit Card 21/09/2022 Kanyon 293.25 45-54
99453 Male Food and Beverage 2 10.46 Cash 22/09/2021 Forum Istanbul 20.92 25-34
99454 Male Food and Beverage 2 10.46 Debit Card 28/03/2021 Metrocity 20.92 55-64
99455 Male Technology 4 4200.00 Cash 16/03/2021 Istinye Park 16800.00 55-64
99456 Female Souvenir 3 35.19 Credit Card 15/10/2022 Mall of Istanbul 105.57 35-44

98346 rows × 9 columns

3. EDA¶

Trước khi thực hiện việc khai phá dữ liệu, nhóm sẽ thực hiện phân tích sơ bộ tập dữ liệu hiện tại thông qua biểu đồ trực quan để hiểu hơn về nghiệp vụ trước khi thực hiện khai phá.

In [107]:
import seaborn as sns
import plotly.express as px

3.1. Category wise¶

Đầu tiên, danh mục sản phẩm phổ biến nhất trên tổng số lượng sản phẩm trong mỗi giao dịch.

In [108]:
category = transactions.groupby('category')['quantity'].sum()
category = pd.DataFrame({'category': category.index, 'quantity': category.values})
category['categories'] = 'categories'

fig = px.treemap(category, path=['categories', 'category'], values='quantity', color='quantity',
                 hover_data=['category'], color_continuous_scale='Blues')
fig.update_layout(width=1000, height=600, paper_bgcolor='LightSteelBlue')
fig.show(renderer='notebook')

Như vậy, sản phẩm thuộc danh mục Clothing, Cosmetics, và Food and Beverage xuất hiện nhiều nhất trong toàn bộ số giao dịch.

3.2. Gender wise¶

Đáng lưu ý, Clothing và Cosmetics là hai danh mục sản phẩm trên thực tế thường được mua bởi phụ nữ, nên có thể số lượng khách hàng nữ cao hơn nam.

In [109]:
transactions['gender'].value_counts()
Out[109]:
Female    59482
Male      39975
Name: gender, dtype: int64

Với số lượng khách hàng nữ cao hơn gần 20000, doanh thu có thể phần lớn đến từ khách hàng nữ.

In [110]:
gender = transactions.groupby('gender')['total'].sum()
gender = pd.DataFrame({'gender': gender.index, 'total': gender.values})

fig = px.pie(gender, values='total', names='gender')
fig.update_layout(paper_bgcolor='LightSteelBlue')
fig.show(renderer='notebook')

Đúng như dự đoán, gần 60% doanh thu đến từ khách hàng nữ.

In [111]:
gender_category = transactions.groupby(['gender', 'category'])['total'].sum().unstack().reset_index()

fig = px.bar(gender_category,
             x=['Books', 'Clothing', 'Cosmetics', 'Food and Beverage', 'Shoes', 'Souvenir', 'Technology', 'Toys'],
             y='gender')
fig.update_layout(width=1000, height=600, plot_bgcolor='LightSteelBlue', paper_bgcolor='LightSteelBlue',
                  legend=dict(title='category'))
fig.show(renderer='notebook')

Với mỗi danh mục sản phẩm, khách hàng nữ đều chi nhiều hơn khách hàng nam khi mua sắm. Tuy nhiên, đây cũng có thể là vì số lượng khách hàng nữ cao hơn. Vì vậy, nhóm không thể dựa vào biểu đồ trực quan như trên để đưa ra quyết định nghiệp vụ marketing hoặc xây dựng hệ thống recommendation. Thay vào đó, để đưa ra chiến lược nhằm duy trì mối quan hệ khách hàng chính xác và hiệu quả, nhóm cần thực hiện quá trình khai phá dữ liệu.

4. Data mining¶

Mục tiêu chính của nhóm là xác định phân khúc khách hàng thân thiết hoặc sản phẩm có giá trị doanh nghiệp cao dựa trên thuật toán phân cụm (Clustering) và phân loại (Classification). Ngoài ra, thuật toán kết hợp (Associate) cũng sẽ được sử dụng để phân tích hành vi mua hàng của khách hàng và xu hướng, khuôn mẫu có ích cho quyết định nghiệp vụ.

4.2. Classification¶

Phân loại là quá trình gồm hai bước: learning và predicting. Trong bước learning, mô hình phân loại được hình thành sử dụng tập dữ liệu training. Trong bước predicting, mô hình trên sẽ được sử dụng để đưa ra dự đoán dựa trên đầu vào. Phân loại, khác với phân cụm, là mô hình học máy có giám sát. Nhóm sẽ sử dụng thuật toán Decision Tree vì thuật toán dễ trực quan hóa và dễ hiểu.

In [112]:
from sklearn import preprocessing
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn import metrics
from sklearn.model_selection import GridSearchCV
from sklearn.tree import export_graphviz
from six import StringIO
from IPython.display import Image
import pydotplus

pd.options.mode.chained_assignment = None
4.2.1. Selecting features¶

Do Decision Tree là mô hình học máy có giám sát, nhóm sẽ xác định biến giải thích (feature variables) và biến kết quả (target variables) trong nghiệp vụ phân loại giới tính khách hàng.

In [113]:
features = ['age_group', 'category', 'quantity', 'payment_method', 'total']
targets = ['gender']
X = transactions[features]
y = transactions[targets]
4.2.2. Transforming data¶

Với hệ thống học máy có nền tảng mạnh, cột có kiểu phân loại được xử lý một cách tự nhiên như ngôn ngữ R sẽ sử dụng factors, hoặc Weka sẽ sử dụng kiểu nominal. Mô hình Decision Tree nhóm sử dụng từ thư viện scikit-learn chỉ chấp nhận biến giải thích (feature variables) kiểu số và liên tục (continuous numerical variables). Để chuyển đổi kiểu dữ liệu, nhóm có hai lựa chọn: one-hot-encoding và label-encoding. Tuy nhiên, khi sử dụng label-encoding trên một cột, mô hình học máy có thể vô tình xem cột đó có thứ tự hoặc cấp bậc. Nhóm có thể mong muốn việc này với cột độ tuổi, tuy nhiên, cột danh mục sản phẩm và phương thức thanh toán không nên có. Sử dụng label-encoding trên cột độ tuổi.

In [114]:
label_encoder = preprocessing.LabelEncoder()
label_encoder.fit(X.age_group)
label_encoder.classes_
Out[114]:
array(['18-24', '25-34', '35-44', '45-54', '55-64', '65-70', nan],
      dtype=object)

Thay thế cột độ tuổi ban đầu.

In [115]:
X['age_group'] = label_encoder.fit_transform(X['age_group'])
X.sample(5)
Out[115]:
age_group category quantity payment_method total
13392 4 Clothing 5 Cash 7502.00
37302 2 Clothing 4 Cash 4801.28
98365 4 Food and Beverage 4 Cash 83.68
7829 3 Clothing 4 Debit Card 4801.28
47253 1 Books 1 Cash 15.15

Sử dụng one-hot-encoding trên cột danh mục sản phẩm và phương thức thanh toán.

In [116]:
X = pd.get_dummies(X, columns=['category', 'payment_method'])
X.sample(5)
Out[116]:
age_group quantity total category_Books category_Clothing category_Cosmetics category_Food and Beverage category_Shoes category_Souvenir category_Technology category_Toys payment_method_Cash payment_method_Credit Card payment_method_Debit Card
66912 0 1 40.66 0 0 1 0 0 0 0 0 1 0 0
89263 6 5 293.25 0 0 0 0 0 1 0 0 0 1 0
2415 0 4 16800.00 0 0 0 0 0 0 1 0 0 1 0
949 0 3 365.94 0 0 1 0 0 0 0 0 1 0 0
82810 4 3 2700.72 0 1 0 0 0 0 0 0 0 1 0

Ngoài ra, biến kết quả (target variables) giới tính cũng nên được áp dụng label-encoding.

In [117]:
label_encoder = preprocessing.LabelEncoder()
label_encoder.fit(y.gender)
label_encoder.classes_
Out[117]:
array(['Female', 'Male'], dtype=object)

Thay thế cột giới tính ban đầu.

In [118]:
y['gender'] = label_encoder.fit_transform(y['gender'])
y.sample(5)
Out[118]:
gender
44695 1
22231 0
36260 1
48147 1
62241 0
4.2.3. Splitting data¶

Để xét độ chính xác của mô hình, nhóm sẽ chia tập dữ liệu thành tập dữ liệu dành cho training và tập dữ liệu dành cho testing. Nhóm sẽ dành ra 70% giao dịch từ tập dữ liệu ban đầu cho việc training và 30% cho việc testing.

In [119]:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=1, stratify=y)
4.2.4. Building model¶

Bắt đầu với việc fit tập dữ liệu training vào mô hình Decision Tree.

In [120]:
clf = DecisionTreeClassifier()
clf = clf.fit(X_train, y_train)

Tiếp theo, dự đoán biến kết quả (target variables) với đầu vào là tập dữ liệu testing X chứa biến giải thích (feature variables).

In [121]:
y_pred = clf.predict(X_test)
y_pred[:5]
Out[121]:
array([0, 0, 0, 0, 0])
4.2.5. Evaluating model¶

Xét độ chính xác của mô hình bằng phương pháp so sánh giữa tập dữ liệu testing y chứa biến kết quả (target variables) và tập dữ liệu dự đoán trên.

In [122]:
metrics.accuracy_score(y_test, y_pred)
Out[122]:
0.5924324686641196
4.2.6. Improving accuracy¶

Hyper-parameters là tham số có thể định nghĩa lúc xây dựng mô hình học máy. Với Decision Tree, việc cấu hình quy luật thuật toán phân chia dữ liệu (theo entropy hay gini impurity) hoặc chiều sâu tối đa có thể giúp tăng độ chính xác của mô hình và tránh overfitting. Việc tìm ra tổ hợp tham số tốt nhất cho mô hình có thể được tự động hóa sử dụng GridSearchCV. Đầu tiên, nhóm sẽ xác định tham số nhóm muốn thay đổi. GridSearchCV thực hiện cross-validation đối với từng tổ hợp tham số trên và xác định tổ hợp tham số tốt nhất.

In [123]:
params = {
    'criterion': ['gini', 'entropy'],
    'max_depth': [None, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
    'max_features': [None, 'sqrt', 'log2', 0.2, 0.4, 0.6, 0.8] + list(range(1, 10)),
    'splitter': ['best', 'random']
}

clf = GridSearchCV(estimator=DecisionTreeClassifier(), param_grid=params, cv=5, n_jobs=-1, verbose=1)
clf.fit(X_train, y_train)
clf.best_params_
Fitting 5 folds for each of 704 candidates, totalling 3520 fits
Out[123]:
{'criterion': 'gini',
 'max_depth': 5,
 'max_features': 0.2,
 'splitter': 'random'}

Sử dụng tổ hợp tham số tốt nhất GridSearchCV tìm được để xây dựng lại mô hình.

In [124]:
clf = DecisionTreeClassifier(criterion=clf.best_params_['criterion'], splitter=clf.best_params_['splitter'],
                             max_depth=clf.best_params_['max_depth'], max_features=clf.best_params_['max_features'])
clf = clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
metrics.accuracy_score(y_test, y_pred)
Out[124]:
0.5981634157785375

Với tổ hợp tham số mới, độ chính xác của mô hình tăng nhẹ và mô hình không còn bị overfitting.

4.2.7. Visualizing model¶

Biểu đồ trực quan Decision Tree cho thấy cấu trúc mô hình học máy với mỗi ô chữ nhật là một nút. Nội dung một nút cho biết quy luật thuật toán phân chia dữ liệu tại dòng đầu tiên và biến kết quả (target variables) tại dòng cuối cùng.

In [125]:
dot_data = StringIO()
export_graphviz(clf, out_file=dot_data, filled=True, rounded=True, special_characters=True, feature_names=X.columns,
                class_names=['0', '1'])
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
graph.write_png('gender.png')
dot_data = StringIO()
Image(graph.create_png())
Out[125]: